Skip to content

Add variable composition and Handlebars template rendering#1731

Closed
dmontagu wants to merge 49 commits into
codex/variable-structure-refactorfrom
feature/variable-composition
Closed

Add variable composition and Handlebars template rendering#1731
dmontagu wants to merge 49 commits into
codex/variable-structure-refactorfrom
feature/variable-composition

Conversation

@dmontagu
Copy link
Copy Markdown
Contributor

@dmontagu dmontagu commented Feb 24, 2026

Summary

  • Variable composition: Managed variable values can reference other managed variables with @{variable_name}@ syntax. Composition happens during resolution, before deserialization, and supports nested references, dotted access like @{brand.tagline}@, escaping with \@{...}@, cycle/depth protection, and composed_from metadata on resolved variables and spans.
  • Template rendering: Variable values can contain Handlebars {{placeholder}} templates rendered with runtime inputs. Rendering uses pydantic-handlebars and preserves {{...}} placeholders while @{...}@ composition is expanded first.
  • TemplateVariable[T, InputsT] API: logfire.template_var() adds a single-step flow where get(inputs) resolves the variable, expands @{...}@ references, renders {{...}} placeholders, and deserializes to the declared type.
  • Explicit render path for regular variables: logfire.var(..., template_inputs=Inputs) records the template input schema and lets callers resolve first, then call resolved.render(inputs) when they want rendering.
  • Validation and sync support: Template input schemas are included in variable config/sync, and validation checks composed values for undeclared template fields plus missing references and reference cycles.
  • Docs and demo: Adds managed-variable docs for templates/composition and a runnable demo covering composition, structured variables, template inputs, and composition-time conditionals.

Syntax

Composition references use @{...}@ to avoid conflicting with prompt/runtime templating languages:

@{safety_rules}@
@{brand.tagline}@
@{#if beta_enabled}@Try beta features@{else}@Welcome@{/if}@

Runtime template inputs continue to use normal Handlebars syntax:

Hello {{user_name}}. @{safety_rules}@

Resolution order:

resolve managed variable
→ expand @{other_variable}@ references
→ render {{runtime_input}} placeholders
→ deserialize/validate to the declared type

Main modules

Module Purpose
logfire/variables/composition.py Reference discovery and expansion with cycle/depth handling
logfire/variables/reference_syntax.py @{...}@ rendering shim that preserves runtime {{...}} placeholders
logfire/variables/template_validation.py Template field validation and composition cycle detection
logfire/variables/variable.py Shared resolution pipeline plus Variable and TemplateVariable

Test plan

  • uv run pytest tests/test_variable_composition.py tests/test_variable_templates.py tests/test_template_validation.py tests/test_variables.py -q
  • uv run pre-commit run --files docs/reference/advanced/managed-variables/index.md docs/reference/advanced/managed-variables/templates-and-composition.md examples/python/variable_composition_demo.py logfire/_internal/main.py logfire/variables/abstract.py logfire/variables/composition.py logfire/variables/reference_syntax.py logfire/variables/template_validation.py logfire/variables/variable.py tests/test_template_validation.py tests/test_variable_composition.py tests/test_variable_templates.py
  • uv run mkdocs build --no-strict

devin-ai-integration[bot]

This comment was marked as resolved.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Feb 24, 2026

Deploying logfire-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: c7ae5b9
Status: ✅  Deploy successful!
Preview URL: https://1443a1dc.logfire-docs.pages.dev
Branch Preview URL: https://feature-variable-composition.logfire-docs.pages.dev

View logs

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…ame>> reference expansion, and template validation
@dmontagu dmontagu force-pushed the feature/variable-composition branch from d414e21 to 9d5a27e Compare March 7, 2026 18:51
devin-ai-integration[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@petyosi
Copy link
Copy Markdown
Member

petyosi commented May 8, 2026

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 8, 2026

@cubic-dev-ai

@petyosi I have started the AI code review. It will take a few minutes to complete.

cubic-dev-ai[bot]

This comment was marked as resolved.

@petyosi
Copy link
Copy Markdown
Member

petyosi commented May 8, 2026

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 8, 2026

@cubic-dev-ai

@petyosi I have started the AI code review. It will take a few minutes to complete.

cubic-dev-ai[bot]

This comment was marked as resolved.

test_unserializable_default_with_references_falls_back needs composition to run (and therefore pydantic_handlebars), but its enclosing class isn't @requires_handlebars. On Python 3.9 the [variables] extra doesn't install pydantic_handlebars and the test fails. Decorate the one test that needs it; the sibling test_unserializable_default_skips_default_composition stays unconditional because it exercises the no-composition short-circuit.
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 21, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment thread logfire/variables/__init__.py
ComposedReference, VariableCompositionError, VariableCompositionCycleError, and ResolutionReason were imported by logfire/variables/__init__.py but missing from __all__, so they weren't picked up by 'from logfire.variables import *' or by auto-generated API docs. They are part of the public surface — ComposedReference is the element type of ResolvedVariable.composed_from and ResolutionReason is the type of ResolvedVariable.reason. Add them to __all__ and import ResolutionReason explicitly.
@alexmojaki alexmojaki mentioned this pull request May 21, 2026
Comment thread docs/reference/advanced/managed-variables/index.md
Comment thread docs/reference/advanced/managed-variables/templates-and-composition.md Outdated
Comment thread docs/reference/advanced/managed-variables/templates-and-composition.md Outdated
Comment thread docs/reference/advanced/managed-variables/templates-and-composition.md Outdated
Comment thread docs/reference/advanced/managed-variables/templates-and-composition.md Outdated
…ic/logfire into feature/variable-composition
@alexmojaki alexmojaki changed the base branch from main to codex/variable-structure-refactor May 21, 2026 15:24
alexmojaki and others added 6 commits May 21, 2026 20:53
- templates-and-composition.md: merge the template_var definition and usage
  into one runnable snippet that covers the conditional case, drop the
  one-row parameters table in favor of prose, and remove the unnecessary
  skip="true" on the composition example.
- index.md: add the missing logfire.configure() call so the snippet does
  not raise LogfireNotConfiguredWarning and can run unskipped.
_BaseVariable no longer carries any template-related surface area: the
public get_template_inputs_schema method is removed and to_config no
longer emits a template_inputs_schema field. TemplateVariable now
overrides to_config to attach its schema, and external diff/sync code
uses a small module-level helper (get_template_inputs_schema) gated on
isinstance(variable, TemplateVariable).

Also tightens _resolve_code_default (formerly _resolve_serialized_default)
so the user's default-resolution function is invoked at most once per
get(), and hardens the outer error handler against the default also
raising while building the error result.
Extracts _BaseVariable._lookup_serialized to encode the
override -> provider -> registered code default priority once, and
routes both _resolve (for self.name) and the composition expander's
resolve_ref (for child @{ref}@ lookups) through it. The two paths can
no longer drift.

Behavioural notes:

- context_override now returns the serialized form from _lookup_serialized;
  _resolve detects the reason and skips composition (preserving the
  literal-override semantics), then optionally renders for TemplateVariable.
- When _lookup_serialized falls back to a registered code default, the
  caller in _resolve promotes the success reason to 'code_default' and
  preserves the provider's exception (matching the previous behaviour
  carved out in _resolve_code_default).
@dmontagu
Copy link
Copy Markdown
Contributor Author

Reopened with a cleaner two-commit history in #1951 (one commit for code, one for docs/demo). End state is byte-identical to this branch — verified by git diff feature/variable-composition..feature/variable-composition-clean being empty. This PR will be closed once #1951 is reviewed and merged.

Composition expands `@{...}@` references through `pydantic-handlebars`
via `logfire/variables/reference_syntax.py:render_once`, so any doc
example using `@{...}@` requires the `[variables]` extra (Python 3.10+).
Extend the test_docs skip predicate to cover that, matching the
existing `logfire.template_var` skip.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants